<template>
  <div
    class="article-editing-area"
    :contenteditable="contentEditable"
    @input="inputHandle"
    @keydown.enter="beforeEnterHandle"
  ></div>
</template>

<script>
export default {
  name: 'EditArea',
  components: {},
  props: {
    articleContent: Object,
    innerHtml: String,
    editable: String,
    id: String
  },
  data () {
    return {
      contentEditable: true,
      // 只记录编辑区中的光标位置
      currSelectionRange: {},
      fixDelayList: {}
    }
  },
  watch: {
    editable (newVal, oldVal) {
      this.contentEditable = newVal
    },
    contentEditable (newVal, oldVal) {
      if (newVal) {
        this.initListeners()
        // 空词条加上零宽不连字
        let entrys = this.$el.getElementsByClassName('entry')
        for (let i = 0; i < entrys.length; i++) {
          if (entrys[i].textContent.length === 0) {
            entrys[i].innerHTML = '&zwnj;'
          }
        }
      } else {
        this.removeListeners()
        // 空词条去除零宽不连字
        let entrys = this.$el.getElementsByClassName('entry')
        for (let i = 0; i < entrys.length; i++) {
          let newInnerHtml = entrys[i].innerHTML.replace(/\u200c/gi, '')
          if (newInnerHtml !== entrys[i].innerHTML) {
            entrys[i].innerHTML = newInnerHtml
          }
        }
      }
    }
  },
  mounted () {
    this.contentEditable = this.editable
    this.$el.innerHTML =
      this.innerHtml ||
      this.deserialize(this.articleContent) ||
      `<div class="p" id="${this.idGenerator()}"><br></div>`
    if (this.editable) {
      // 初始化监听器
      this.initListeners()
      // 空词条加上零宽不连字
      let entrys = this.$el.getElementsByClassName('entry')
      for (let i = 0; i < entrys.length; i++) {
        if (entrys[i].textContent.length === 0) {
          entrys[i].innerHTML = '&zwnj;'
        }
      }
    }
  },
  destroyed () {
    this.removeListeners()
  },
  methods: {
    isEditable (bool) {
      this.contentEditable = bool
    },
    // 生成随机id
    idGenerator () {
      return Math.random()
        .toString(36)
        .slice(-6)
    },
    initListeners () {
      // 光标选区修改事件
      document.addEventListener('selectionchange', this.selectionChangeHandle)
      // 拖拽事件开始
      this.$el.addEventListener('dragstart', this.dragStartHandle)
      // 在字符输入前
      this.$el.addEventListener('beforeinput', this.beforeInputHandle)
    },
    removeListeners () {
      document.removeEventListener(
        'selectionchange',
        this.selectionChangeHandle
      )
      this.$el.removeEventListener('dragstart', this.dragStartHandle)
      this.$el.removeEventListener('beforeinput', this.beforeInputHandle)
    },
    selectionChangeHandle (e) {
      if (e.target.activeElement !== this.$el) {
        // 修复所有等待修复的段落
        this.fixParagraphElementFromDelayList(true)
        return
      }
      this.cursorChangeHandle()
    },
    beforeInputHandle (e) {
      let range = this.currSelectionRange
      let endEl =
        range.endTextEl.nodeType === 3
          ? range.endTextEl.parentElement
          : range.endTextEl
      let startEl =
        range.startTextEl.nodeType === 3
          ? range.startTextEl.parentElement
          : range.startTextEl
      // 进行删除操作时
      if (
        e.inputType === 'deleteContentForward' ||
        e.inputType === 'deleteContentBackward'
      ) {
        let currFocusParagraphEl = range.endParagraphEl
        // 文章内容全部删除的情况,首段落innerHTML固定为<br>
        if (
          this.$el.childElementCount === 1 &&
          (currFocusParagraphEl.innerHTML === '' ||
            currFocusParagraphEl.innerHTML === '<br>')
        ) {
          currFocusParagraphEl.innerHTML = '<br>'
          e.preventDefault()
          return false
        }
        // 段落结尾加上零宽不连字,在input完成后再删除
        if (currFocusParagraphEl.textContent !== '') {
          let lastChildEl = currFocusParagraphEl.lastChild
          if (lastChildEl && lastChildEl.className !== 'pEndPadding') {
            // 非空自然段结尾加零宽不连字
            currFocusParagraphEl.insertAdjacentHTML(
              'beforeend',
              '<span id="pEndPadding" class="pEndPadding">&zwnj;</span>'
            )
          }
        }
        // 删除操作时,entry加上零宽不连字填充
        if (range.isCollapsed) {
          if (e.inputType === 'deleteContentForward') {
            // 光标处于entry前&&entry.textContent.length==1
            let nextEl = null
            let el = range.startTextEl
            // 获取光标所在的父元素,此元素的下一个兄弟节点不为空
            while (el && el.parentElement !== this.$el) {
              if ((nextEl = el.nextElementSibling)) {
                break
              }
              el = el.parentElement
            }
            // 向下查找上面获得的兄弟节点,获取兄弟节点中第一个包裹textNode的子节点
            while (nextEl) {
              if (nextEl.nodeType === 3) {
                nextEl = nextEl.parentElement
                break
              }
              nextEl = nextEl.firstChild
            }
            // 节点类型为entry,且内容字符串长度等于1
            if (
              nextEl &&
              nextEl.className === 'entry' &&
              nextEl.textContent.replace(/\u200c/gi, '').length === 1
            ) {
              nextEl.insertAdjacentHTML('beforeend', '&zwnj;')
            }
          } else if (e.inputType === 'deleteContentBackward') {
            // entry元素结尾&&entry.textContent.length==1
            if (
              startEl.className === 'entry' &&
              startEl.textContent.replace(/\u200c/gi, '').length === 1
            ) {
              startEl.insertAdjacentHTML('afterbegin', '&zwnj;')
            }
          }
        }
        return
      }
      // 输入操作时
      // 粘贴操作
      if (e.inputType === 'insertFromPaste') {
        if (!this.pasteHandle(e)) {
          e.preventDefault()
          return false
        }
        // 如果是空段落,且粘贴内容带有换行符,先插入零宽不连字,防止生成新段落失败
        if (range.startParagraphEl.textContent === '') {
          let res = this.getPlainData(e.dataTransfer)
          if (res.data.indexOf('\n') !== -1) {
            endEl.insertAdjacentHTML('afterbegin', '&zwnj;')
          }
        }
      }
      // 整个entry元素刚好处于拖蓝选区起始或结束位置时,需要在entry元素加上零宽不连字防止被删除
      if (!range.isCollapsed) {
        if (
          endEl.className === 'entry' &&
          range.endTextEl.nodeValue.length === range.endOffset
        ) {
          endEl.insertAdjacentHTML('beforeend', '&zwnj;')
        }
        if (
          startEl.className === 'entry' &&
          startEl !== endEl &&
          range.startOffset === 0
        ) {
          startEl.insertAdjacentHTML('afterbegin', '&zwnj;')
        }
      }
    },
    inputHandle (event) {
      let range = this.currSelectionRange
      let startP = range.startParagraphEl
      this.cursorChangeHandle()
      let endP = this.currSelectionRange.startParagraphEl
      // 在段落首部换行的情况,需要对上一行进行处理
      if (
        event.inputType === 'insertParagraph' &&
        range.isCollapsed &&
        range.isFirstTextNodeInParagraph &&
        range.startOffset === 0
      ) {
        startP = startP.previousElementSibling
      }
      // 段落必须是存在的
      if (!this.$el.contains(startP)) {
        startP = endP
      } else if (!this.$el.contains(endP)) {
        endP = startP
      }
      // 处理beforeInputHandle中添加的元素
      let pEl = startP
      while (pEl) {
        let entrys = pEl.getElementsByClassName('entry')
        for (let i = 0; i < entrys.length; i++) {
          if (
            entrys[i].innerHTML.length > 1 &&
            entrys[i].innerHTML.search(/\u200c/i) !== -1
          ) {
            // entry是最基础的元素,innerHtml不允许标签存在
            let newinnerHtml = entrys[i].textContent
              .replace(/\u200c/gi, '')
              .replace('\n', '')
            entrys[i].innerHTML = newinnerHtml
            // 重新定位光标位置
            window
              .getSelection()
              .collapse(entrys[i].firstChild, newinnerHtml.length)
          }
        }
        if (pEl === endP) {
          break
        }
        pEl = pEl.nextElementSibling
      }
      // 处理修改过的段落
      // console.log(startP)
      // console.log(endP)
      pEl = startP
      let pIds = {}
      while (pEl && pEl !== endP) {
        // 为段落生成新的id
        if (!pEl.id || pIds[pEl.id]) {
          // 如果id已存在,生成新id
          pEl.id = this.idGenerator()
        } else {
          pIds[pEl.id] = pEl.id
        }
        if (pEl.textContent !== '') {
          // 修复标签,输入的文本存在换行符或删除操作导致生成新段落,标签被分割,需要给标签id重新赋值
          this.fixEntryElement(pEl)
          this.fixSencenteElment(pEl)
          this.fixClusterElment(pEl)
        } else {
          pEl.innerHTML = '<br>'
        }
        pEl = pEl.nextElementSibling
      }
      // 处理当前节点
      // id为空或者id已存在,说明节点刚添加,初始化后直接返回
      if (!endP.id || pIds[endP.id]) {
        endP.className = endP.className ? endP.className : 'p'
        endP.id = this.idGenerator()
      }
      // 段落可能出现innerHTML中只有零宽不连字的情况,需要先添加<br>
      if (
        endP.childElementCount === 1 &&
        endP.firstChild.id === 'pEndPadding'
      ) {
        endP.insertAdjacentHTML('afterbegin', '<br>')
      }
      // 删除零宽不连字
      let pEndPaddingEl = document.getElementById('pEndPadding')
      if (pEndPaddingEl) {
        pEndPaddingEl.parentElement.removeChild(pEndPaddingEl)
      }
      if (endP.textContent.replace(/\u200c/gi, '') === '') {
        endP.innerHTML = '<br>'
        return
      }
      // 存放到延时列表
      this.fixDelayList[endP.id] = endP
      // 修复后的html返回给组件外层的input监听
      this.$emit('input', this.id, this.$el.innerHTML)
    },
    // 监听光标移动事件
    cursorChangeHandle () {
      // 记录当前的选中区起始节点
      this.recordCurrSelection()
      // 修复fixDelayList列表中的段落,不修复已被选中的段落
      this.fixParagraphElementFromDelayList()
    },
    isFirstTextNodeInParagraph (textEl) {
      let el = textEl
      while (el && el.parentElement !== this.$el) {
        if (el.parentElement.firstChild !== el) {
          return false
        }
        el = el.parentElement
      }
      return true
    },
    // 记录选区的开始位置
    recordCurrSelection () {
      let selection = window.getSelection()
      let range = selection.getRangeAt(0)
      let startTextEl = range.startContainer
      let startP = this.getParagraphConteiner(startTextEl)
      let endTextEl = range.endContainer
      let endP = startP
      if (!range.isCollapsed) {
        endP = this.getParagraphConteiner(endTextEl)
      }
      // 判断拖蓝的方向
      let toRight = null
      if (!range.collapsed) {
        if (startTextEl !== endTextEl) {
          toRight = selection.anchorNode === startTextEl
        } else {
          toRight = selection.anchorOffset <= selection.focusOffset
        }
      }
      // 更新选区
      this.currSelectionRange = {
        isFirstTextNodeInParagraph: this.isFirstTextNodeInParagraph(
          startTextEl
        ),
        toRight: toRight,
        isCollapsed: range.collapsed,
        startTextEl: startTextEl,
        startParagraphEl: startP,
        startOffset: range.startOffset,
        endTextEl: endTextEl,
        endParagraphEl: endP,
        endOffset: range.endOffset
      }
      this.$emit('cursorChange', this.currSelectionRange)
    },
    // 获取元素所在的段落节点
    getParagraphConteiner (el) {
      while (el.parentElement && el.parentElement !== this.$el) {
        el = el.parentElement
      }
      return el
    },
    // 修复光标选区外的段落
    fixParagraphElementFromDelayList (fixAll) {
      if (Object.keys(this.fixDelayList).length === 0) {
        return
      }
      // 处理所有需要修复的节点
      // 获取需要修复的起始段落和结束段落
      let startEl = this.currSelectionRange.startParagraphEl
      let endEl = this.currSelectionRange.endParagraphEl
      let pEl = fixAll ? null : startEl
      let pEls = []
      while (pEl) {
        pEls.push(pEl.id)
        if (pEl === endEl) {
          break
        }
        pEl = pEl.nextElementSibling
      }
      // console.log(JSON.stringify(this.fixDelayList));
      // 修复
      for (let key in this.fixDelayList) {
        if (key && pEls.indexOf(key) === -1) {
          let pEl = this.fixDelayList[key]
          delete this.fixDelayList[key]
          if (!pEl || pEl.textContent === '') {
            continue
          }
          this.fixEntryElement(pEl)
          this.fixSencenteElment(pEl)
          this.fixClusterElment(pEl)
        }
      }
      // console.log(JSON.stringify(this.fixDelayList));
    },
    // 监听回车前事件
    beforeEnterHandle (e) {
      if (!this.currSelectionRange || !this.currSelectionRange.startNode) {
        return
      }
      // 词条中需要对换行操作进行确认
      if (this.currSelectionRange.startNode.className === 'entry') {
        if (!confirm('换行符将破坏词条完整性,是否粘贴?')) {
          e.preventDefault()
        }
      }
    },
    // 监听粘贴操作,不允许粘贴含有标签的内容
    pasteHandle (e) {
      // 获取粘贴板内容
      let res = this.getPlainData(e.dataTransfer)
      // 如果不是纯文本,提示是否粘贴
      if (res.isHtml) {
        if (!confirm('已自动转换为纯文本,是否粘贴?')) {
          return false
        }
        // document.execCommand('insertHTML', false, res.data)
        // e.preventDefault()
        return true
      }
      // 如果包含换行符,检查当前聚焦的元素是否词条
      if (res.data.indexOf('\n') !== -1) {
        if (
          this.currSelectionRange.startTextEl.parentElement.className ===
          'entry'
        ) {
          if (!confirm('换行符将破坏词条完整性,是否粘贴?')) {
            return false
          }
        }
      }
      return true
    },
    // 监听拖拽操作
    dragStartHandle (e) {
      // 获取粘贴板内容
      let res = this.getPlainData(e.dataTransfer)
      // 修改拖动的数据
      // TODO 如果存在换行符,用一个toast进行提示??
      e.dataTransfer.setData('text/plain', res.data)
      e.dataTransfer.clearData('text/html')
    },
    // 判断DataTransfer对象中存放的数据类型，返回纯文本
    getPlainData (dataTransfer) {
      let data = dataTransfer.getData('text/html')
      let res = { isHtml: false, data: '' }
      if (data) {
        res.isHtml = true
        res.data = dataTransfer.getData('text/plain')
        if (!res.data) {
          res.data = data
        }
        res.data = res.data
          .replace(/(^\s*)|(\s*$)/g, '')
          .replace(/<div( .+?=".+?")*?>(.+?)<\/div>/g, '$1\r\n')
          .replace(/<.+?>/g, '')
      } else {
        res.isHtml = false
        res.data = dataTransfer.getData('text/plain')
        if (!res.data) {
          res.data = ''
        }
      }
      return res
    },
    // 去除修改内容后产生的无效词条节点
    fixEntryElement (pEl) {
      let entryEls = pEl.getElementsByClassName('entry')
      for (let i = 0; i < entryEls.length; i++) {
        let entryEl = entryEls[i]
        // 普通句子的词条中包含断句符号,需要进行处理
        if (entryEl.parentElement.classList.contains('s')) {
          let spiltRes = /(.*[。？！；][”]?)(.+)/.exec(entryEl.textContent)
          if (spiltRes && spiltRes.length === 3) {
            entryEl.innerHTML = spiltRes[2]
            entryEl.before(document.createTextNode(spiltRes[1]))
          }
        }
      }
    },
    // 格式化段落的html
    formatParagraphInnerHtml (innerHtml) {
      // 为排除干扰,先使用特殊转义符替换词条
      innerHtml = innerHtml.replaceAll(
        /(<[^<>]+? class="entry( .+?)*?"[^<>]*?>)(.*?)<\/[^<>]+?>/g,
        (p1, p2, p3, p4) => {
          let getTagId = /.+ id="([^"]+?)".*/.exec(p2)
          let tagId = getTagId ? getTagId[1] : 'undefine'
          let newTag = '#{entry|' + tagId + '}'
          return newTag + p4 + newTag
        }
      )
      // 为排除干扰,先使用特殊转义符替换句子聚簇标签
      innerHtml = innerHtml.replaceAll(
        /(<[^<>]+? class="cluster( .+?)*?"[^<>]*?>)(.*?)<\/[^<>]+?>/g,
        (p1, p2, p3, p4) => {
          let getTagId = /.+ id="([^"]+?)".*/.exec(p2)
          let tagId = getTagId ? getTagId[1] : 'undefine'
          let newTag = '#{cluster|' + tagId + '}'
          return newTag + p4 + newTag
        }
      )
      // 开始根据标点划分句子
      innerHtml = innerHtml.replaceAll(
        /#\{cluster\|[^#]+?\}(.*?)#\{cluster\|[^#]+?\}|.+?[。？！；]((#\{.+?\|.+?\})*[”])?(<\/[^<>]+?>)?|.+?$/g,
        sencenteWithTag => {
          if (
            sencenteWithTag.search(
              /^#\{cluster\|[^#]+?\}(.*?)#\{cluster\|[^#]+?\}$/
            ) !== -1
          ) {
            // 句子聚簇,直接返回,不做处理
            return sencenteWithTag
          }
          // 获取原本句子标签的属性,就近继承id
          let regTagId = /<[^<>]+? id="(.+?)"[^<>]*?>/.exec(sencenteWithTag)
          // console.log("regTagId="+(regTagId ? regTagId[1] : "null")+" sencenteWithTag="+sencenteWithTag);
          let tagId = regTagId ? regTagId[1] : this.idGenerator()
          // 去除所有标签
          let sencenteWithoutTag = sencenteWithTag.replace(
            /<[^<>]+?>|<\/[^<>]+?>/g,
            ''
          )
          // 重新构建句子,继承id
          return `<span class="s" id="${tagId}">${sencenteWithoutTag}</span>`
        }
      )
      // 划分句子后可能存在词条标签被排除在句子外的情况, 在这里进行修正
      innerHtml = innerHtml.replaceAll(
        /(#\{entry\|.+?\})(.+?#\{entry\|.+?\})/g,
        (p1, p2, p3) => {
          p3 = p3.replace(
            /(.+?)(<\/span><span class="s" .+?>)(#\{entry\|.+?\})$/,
            '$1$3$2'
          )
          return p2 + p3
        }
      )
      innerHtml = innerHtml.replace(
        /(<\/span><span class="s" .+?>)((#\{entry\|.+?\}){2,})(#\{entry\|.+?\})/g,
        '$2$1$4'
      )
      // 重新转换句子聚簇标签
      innerHtml = innerHtml.replaceAll(
        /#\{cluster\|([^#]+?)\}(.*?)#\{cluster\|[^#]+?\}/g,
        (p1, p2, p3) => {
          // 去除聚簇中可能包含的完整句子标签
          p3 = p3.replace(/<[^<>]+?( .+?=".+?")+?>(.+?)<\/[^<>]+?>/g, '$2')
          // 保存不完整的标签
          let prevEndTag = ''
          let nextStartTag = ''
          p3 = p3.replaceAll(/<[^<>]+?( .+?=".+?")+?>|<\/[^<>]+?>/g, p1 => {
            if (p1.search(/^<\/[^<>]+?>$/g) !== -1) {
              prevEndTag = p1
            } else {
              nextStartTag = p1
            }
            return ''
          })
          // 生成句子聚簇标签
          let tagId = p2 !== 'undefine' ? p2 : this.idGenerator()
          let newTag = `<span class="cluster" id=${tagId}>${p3}</span>`
          return prevEndTag + newTag + nextStartTag
        }
      )
      // 重新转换词条标签
      innerHtml = innerHtml.replaceAll(
        /#\{entry\|([^#]+?)\}(.*?)#\{entry\|[^#]+?\}/g,
        (p1, p2, p3) => {
          // 去除词条中可能包含的完整句子标签
          p3 = p3.replace(/<[^<>]+?( .+?=".+?")+?>(.+?)<\/[^<>]+?>/g, '$2')
          // 保存不完整的标签
          let prevEndTag = ''
          let nextStartTag = ''
          p3 = p3.replaceAll(/<[^<>]+?( .+?=".+?")+?>|<\/[^<>]+?>/g, p1 => {
            if (p1.search(/^<\/[^<>]+?>$/g) !== -1) {
              prevEndTag = p1
            } else {
              nextStartTag = p1
            }
            return ''
          })
          let tagId = p2 !== 'undefine' ? p2 : this.idGenerator()
          let content = p3 || '&zwnj;'
          let newTag = `<span class="entry" id="${tagId}">${content}</span>`
          return prevEndTag + newTag + nextStartTag
        }
      )
      // 去除无效的句子标签
      innerHtml = innerHtml.replace(
        /<[^<>]+? class="s"[^<>]*?><\/[^<>]+?>/g,
        ''
      )
      return innerHtml
    },
    // 重新划分句子
    fixSencenteElment (pEl) {
      let innerHtml = pEl.innerHTML.replace(/\u200c/gi, '')
      pEl.innerHTML = this.formatParagraphInnerHtml(innerHtml)
    },
    // 修复句子聚簇
    fixClusterElment (pEl) {
      let clusterEls = pEl.getElementsByClassName('cluster')
      for (let i = 0; i < clusterEls.length; i++) {
        let clusterEl = clusterEls[i]
        if (clusterEl.innerHTML === '') {
          pEl.removeChild(clusterEl)
          return
        }
        if (!clusterEl.id || clusterEl.id === '') {
          // 如果id为空,重新复制id
          clusterEl.id = this.idGenerator()
        }
        if (clusterEl.textContent.search(/.+?[。？！；][”]?$/g) === -1) {
          // 如果聚簇不是以。？！；结束的,合并下一个句子
          let nextNode = clusterEl.nextElementSibling
          if (nextNode) {
            pEl.removeChild(nextNode)
            clusterEl.innerHTML = clusterEl.innerHTML + nextNode.innerHTML
          }
        }
      }
    },
    // 修改选中段落的类型
    changeParagraphType (className) {
      let startP = this.currSelectionRange.startParagraphEl
      let endP = this.currSelectionRange.endParagraphEl
      if (!startP && !endP) {
        return
      }
      let p = startP
      do {
        p.className = className
        if (p === endP) {
          break
        }
      } while ((p = p.nextElementSibling))
      this.$emit('input', this.id, this.$el.innerHTML)
    },
    // 序列化文章内容
    serialize (articleHTMLElement) {
      articleHTMLElement = articleHTMLElement || this.$el
      let pEl = articleHTMLElement.firstChild
      if (!pEl) {
        return null
      }
      let json = { content: [] }
      let innerHtmlContainer = document.createElement('div')
      while (pEl) {
        let pInnerHtml = pEl.innerHTML
        // 转义句集
        let clusterEls = pEl.getElementsByClassName('cluster')
        let clusterEl
        for (let i = 0; i < clusterEls.length; i++) {
          clusterEl = clusterEls[i]
          pInnerHtml = pInnerHtml.replace(
            clusterEl.outerHTML,
            `#{cluster|${clusterEl.id}}${clusterEl.innerHTML}#{cluster|${clusterEl.id}}`
          )
        }
        // 转义词条
        let entryEls = pEl.getElementsByClassName('entry')
        let entryEl
        for (let i = 0; i < entryEls.length; i++) {
          entryEl = entryEls[i]
          pInnerHtml = pInnerHtml.replace(
            entryEl.outerHTML,
            `#{entry|${entryEl.id}}${entryEl.innerHTML}#{entry|${entryEl.id}}`
          )
        }
        innerHtmlContainer.innerHTML = pInnerHtml
        json.content.push({
          id: pEl.id,
          type: pEl.classList[0],
          val: innerHtmlContainer.textContent
        })
        pEl = pEl.nextElementSibling
      }
      return json
    },
    // 反序列化为html
    deserialize (json) {
      if (!json) {
        return null
      }
      if (typeof json === 'string') {
        json = JSON.parse(json)
      }
      let contentList = json.content
      let innerHtml = ''
      for (let i = 0; i < contentList.length; i++) {
        let p = contentList[i]
        let pInnerHtml = p.val
        // // 反序列化词条
        // pInnerHtml = pInnerHtml.replace(
        //   /#\{entry\|([^#]+?)\}(.*?)#\{entry\|[^#]+?\}/g,
        //   '<span class="entry" id="$1">$2</span>'
        // )
        // // 反序列化句集
        // pInnerHtml = pInnerHtml.replace(
        //   /#\{cluster\|([^#]+?)\}(.*?)#\{cluster\|[^#]+?\}/g,
        //   '<span class="cluster" id="$1">$2</span>'
        // )
        pInnerHtml = this.formatParagraphInnerHtml(pInnerHtml)
        innerHtml += `<div class="${p.type}" id="${p.id}">${pInnerHtml}</div>`
      }
      return innerHtml
    }
  }
}
</script>

<style>
.article-editing-area {
  outline: none;
  width: 100%;
  height: 100%;
  line-height: 2em;
}
.article-editing-area::after {
  content: '\200c';
  display: inline-block;
  height: 90%;
}
.entry {
  background-color: rgba(0, 0, 0, 0.05);
  margin-left: 2px;
  margin-right: 2px;
}
.entry:before {
  content: '';
  padding: 0px 0px 0px 2px;
  margin: 0px 2px 0px 0px;
  border-left: 1px solid #4b789f;
  border-top: 1px solid #4b789f;
  border-bottom: 1px solid #4b789f;
}
.entry:after {
  content: '';
  padding: 0px 0px 0px 2px;
  margin: 0px 0px 0px 2px;
  border-right: 1px solid #4b789f;
  border-top: 1px solid #4b789f;
  border-bottom: 1px solid #4b789f;
}
.s {
  padding-top: 0.45em;
  padding-bottom: 0.45em;
  margin: 0 2px;
}
.cluster {
  padding-top: 0.45em;
  padding-bottom: 0.45em;
  margin: 0 2px;
  background-color: rgba(0, 0, 0, 0.05);
}
.cluster:before {
  content: '\200c';
  position: relative;
  display: inline-block;
  width: 3px;
  border-left: 1px solid #d0a024;
  border-top: 1px solid #d0a024;
  border-bottom: 1px solid #d0a024;
}
.cluster:after {
  content: '\200c';
  position: relative;
  display: inline-block;
  width: 3px;
  border-right: 1px solid #d0a024;
  border-top: 1px solid #d0a024;
  border-bottom: 1px solid #d0a024;
}
.p {
  text-indent: 2em;
  padding: 5px;
}
.title {
  font-size: 2em;
  line-height: 2em;
  font-weight: bold;
  text-align: center;
  padding-top: 10px;
}
.sub-title {
  font-weight: bold;
  text-align: center;
  padding: 10px 0;
}
</style>
